﻿#define _CRT_SECURE_NO_WARNINGS

#include"Snake.h"

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	//获得控制台句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置光标位置
	SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎界面
void Welcome()
{
	//设置控制台光标到  合适  的位置
	SetPos(40,15);
	printf("欢迎游玩贪吃蛇");
	SetPos(40, 17);//设置控制台光标到  合适  的位置，方便打印 按任意键继续...

	system("pause");//暂停程序，达成  按任意键继续...  的功能
	system("cls");//清除控制台之前打印的信息
	SetPos(36, 15);
	printf("按↑，↓，←，→即可操纵蛇的方向");
	SetPos(40, 16);
	printf("按F3加速，按F4减速");
	SetPos(24, 17);
	printf("加速时吃掉食物可以获得更高分数，减速时吃掉食物获得分数减少");
	SetPos(26, 18);
	printf("按Esc退出游戏，按空格可暂停游戏，再次按空格时可恢复");
	SetPos(40, 20);
	system("pause");
	system("cls");
}

//打印游戏时的界面
void PlayInte(Snake* snake)
{
	//打印墙
	//上墙体
	int i = 0;
	wchar_t ch = L'■';//宽字符定义并赋值

	for (i = 0; i <COR; i += 2)//因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
	{                          //所以i+=2
		SetPos(i,0);
		wprintf(L"%lc", ch);//打印宽字符
	}

	//下墙体
	for (i = 0; i < COR; i += 2)//因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
                               //所以i+=2
	{
		SetPos(i,ROW-1);
		wprintf(L"%lc", ch);
	}
	
	//左侧墙体
	for (i = 1; i < ROW; i++)//虽然一个宽字符占  2  个单位的横坐标
		                     //但是一个宽字符只占  1  个单位的纵坐标
	{
		SetPos(0,i);
		wprintf(L"%lc", ch);
	}

	//右侧墙体
	for (i = 1; i < ROW; i++)//虽然一个宽字符占  2  个单位的横坐标
		                     //但是一个宽字符只占  1  个单位的纵坐标
    {
		SetPos(COR-2, i);
		wprintf(L"%lc", ch);
	}

	//打印提示信息
	SetPos(65, 16);
	printf("F3加速");
	SetPos(65, 17);
	printf("F4减速");
	SetPos(65, 18);
	printf("Esc退出游戏");
	SetPos(65, 19);
	printf("空格暂停游戏");
	SetPos(65, 20);
	printf("使用↑，↓，→，←控制蛇的方向");
}

//打印蛇身
void SnakePrint(Snake* snake)
{
	SnakeNode* cur = snake->pSnake;
	wchar_t ch1 = L'□';//为了区分蛇头用  蛇头用 □
	wchar_t ch2 = L'●';//蛇身用 ●
	int flag = 0;//用于标识是否为第一次打印

	while (cur)//遍历链表
	{
		SetPos(cur->x, cur->y);//设置控制台光标位置
		if (flag == 0)//为第一次打印就  打印蛇头 □
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);//不为第一次打印就  打印蛇身 ●
		}
		cur = cur->next;
	}
}

//随机生成食物
void RandomFood(Snake* snake)
{
	//蛇身节点与食物的类型一致，吃掉食物的时候就   只需要头插就行
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (newnode == NULL)
	{
		printf("RandomFood函数中malloc失败！");
		return;
	}
	SnakeNode* cur = snake->pSnake;//获得snake中存储的食物信息
	//控制随机食物的坐标生成的范围
	newnode->x = (rand() % ((COR - 3 - 2)/2) + 1)*2;//控制食物的横坐标与蛇一样  是偶数  方便蛇吃
	newnode->y = rand() % (ROW - 1 - 1) + 1;

	//食物不能生成在蛇的身体中
	while (cur)//遍历链表
	{
		if (cur->x == newnode->x && cur->y == newnode->y)
		{   //如果在蛇身中就再随机生成一次坐标
			newnode->x = (rand() % ((COR - 3 - 2) / 2) + 1) * 2;
			newnode->y = rand() % (ROW - 1 - 1) + 1;
			//让cur回到链表开头，重新遍历，防止再次随机生成的食物又再蛇身里
			cur = snake->pSnake;
		}
		else
		{
			cur = cur->next;
		}
	}
	snake->pFood = newnode;//更改snake中存储的食物信息
	SetPos(newnode->x, newnode->y);
	wprintf(L"卍");//打印食物
}

//吃掉食物
void EtaFoodNode(Snake* snake)
{
	//让snake中存储的食物节点的头插在蛇头上
		snake->pFood->next = snake->pSnake;
	snake->pSnake = snake->pFood; //让原食物节点成为蛇头节点
		//打印蛇
		SnakePrint(snake);
}
//下一步不是食物
void NoFoodNode(Snake* snake, SnakeNode* newnode)
{
	//让新节点头插在蛇头上
		newnode->next = snake->pSnake;
	snake->pSnake = newnode;
	SnakeNode* cur = snake->pSnake;
	SnakeNode* prev = snake->pSnake;  //prev指向cur的前一个节点
		wchar_t ch1 = L'□';
	wchar_t ch2 = L'●';
	int flag = 0;
	while (cur->next) // 找尾并打印
	{
		prev = cur;
		SetPos(cur->x, cur->y);
		if (flag == 0)
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);
		}
		cur = cur->next;
	}
	SetPos(cur->x, cur->y);  //找到尾节点的坐标
		printf("  ");  //用空格覆盖尾节点之前打印的  ●
		free(cur);  //释放尾节点
		prev->next = NULL;  //让尾节点的前一个节点的指针域置空
}



//判断下一个节点是不是食物
bool IsFoodNode(Snake* snake,int x,int y)
{
	if (x == snake->pFood->x && y == snake->pFood->y)
		return true;
	else
		return false;
}
//判断是否撞墙
bool IsWall(SnakeNode* newnode)
{
	//因为墙的横坐标只能为0/墙的最大横坐标（COR）-2
	//墙的纵坐标只能是0/ROW-1
	//又因为蛇的横纵坐标不能为墙的横纵坐标
	//所以  已经要连接在蛇头的  下一个节点的坐标为墙的横纵坐标，蛇就撞墙了
	if (newnode->x == 0 || newnode->x == COR - 2 || newnode->y == 0 || newnode->y == ROW - 1)
		return true;
	else
		return false;
}

//判断是否咬到自己
bool IsSnakeNode(Snake* snake, SnakeNode* newnode)
{
	SnakeNode* cur = snake->pSnake;
	while (cur)//遍历蛇身链表
	{
		//已经要连接在蛇头的  下一个节点的坐标与蛇身节点的坐标重合
		//时就要到了自己
		if (cur->x == newnode->x && cur->y == newnode->y)
		{
			return true;
		}
		cur = cur->next;
	}
	return false;
}



//走一步
void SnakeMove(Snake* snake)
{
	//蛇走的方法是  制造一个新节点【新节点为蛇可能的下一个节点】，
	//将新节点头插连接在蛇头上，成为新的头，并根据情况删除尾节点
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));//申请新节点
	if (newnode == NULL)
	{
		printf("NoFoodNode函数中malloc失败！");
		return;
	}
	switch (snake->Snakedir)//获取蛇的方向
	{
	case UP:
		newnode->x=snake->pSnake->x; //蛇的方向为上时下一个节点（新节点）的横坐标不变
		newnode->y=snake->pSnake->y - 1;//蛇的方向为上时下一个节点（新节点）的纵坐标-1
		break;
	case DOWN:
		newnode->x = snake->pSnake->x;
		newnode->y = snake->pSnake->y+1;
		break;
	case LEFT:
		newnode->x = snake->pSnake->x-2;
		newnode->y = snake->pSnake->y;
		break;
	case RIGHT:
		newnode->x = snake->pSnake->x+2;
		newnode->y = snake->pSnake->y;
		break;
	}
	if (IsFoodNode(snake, newnode->x, newnode->y))//如果下一个节点（新节点）为食物
	{
		EtaFoodNode(snake);//吃掉食物
		//随机生成食物
		RandomFood(snake);
		//增加分数
		snake->Score += snake->foodWeight;
	}
	else
	{
		//判断是否撞墙
		if (IsWall(newnode))
		{
			snake->status = KILL_BY_WALL; //将游戏状态置为  因撞墙死亡
			return;//结束函数
		}

		//判断是否咬到自己
		if (IsSnakeNode(snake, newnode))
		{
			snake->status = KILL_BY_SELF;//将游戏状态置为  因咬自己死亡
			return;
		}
		NoFoodNode(snake, newnode);
	}
}

//销毁蛇身链表
void DestSnake(Snake* snake)
{
	SnakeNode* cur = snake->pSnake->next;//cur指向蛇头节点的下一个节点
	SnakeNode* prev = snake->pSnake;//prev指向cur的前一个节点
	snake->pSnake = NULL;//蛇头置空
	while (cur)//遍历链表
	{
		free(prev);//释放prev
		prev = cur;//让prev向后走一步
		cur = cur->next;//让cur向后走一步
	}
	free(prev);//释放尾节点
}

//游戏善后
void GameEnd(Snake* snake)
{
	if (snake->status == KILL_BY_WALL)//如果是因为撞墙死就进去
	{
		system("cls");//清屏
		SetPos(45, 15);

		//打印提示信息
		printf("你撞到墙了，你失败了");
		SetPos(45, 18);
		printf("你的总分为：%d",snake->Score);

		//使用count来统计游玩的次数
		int count = 0;

		//如果是第一次游玩，就还没有chart.txt文件，以  r  的方式打开就会失败
		FILE* pf = fopen("chart.txt", "r+");

		if (pf == NULL)//文件打开失败 fopen函数返回NULL
		{
			//如果是第一次游玩，就还没有chart.txt文件，就再次以w的方式打开并创建chart.txt文件
			pf= fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中，chart.txt文件打开失败");
				exit(-1);
			}
			//把count的值和分数存入chart.txt文件
			fprintf(pf, "%-5d %-10d\n",count+1, snake->Score);
			fclose(pf);//关闭文件
		}
		else
		{
			//如果不是第一次游玩就取出之前游玩时存入的count的值
			
			rewind(pf);//让文件读写指针回到  文件最开头

			fscanf(pf, "%d", &count);//取出之前游玩时存入的count的值

			count++;//取出值后count再++，将这次的游玩也统计

			fseek(pf, 0, SEEK_END);//让文件读写指针定位到文件末尾

			fprintf(pf, "%-10d\n", snake->Score);//在文件最末尾记录这次的分数

			rewind(pf);//再让文件读写指针回到  文件最开头

			fprintf(pf, "%-5d", count);//把++后的count覆盖文件最开头的上一次的count的值
			fclose(pf);//关闭文件
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status == KILL_BY_SELF)//如果是因为咬到自己就进去
	{
		system("cls");
		SetPos(45, 15);
		printf("你咬到自己了，你失败了");
		SetPos(45, 18);
		printf("你的总分为：%d", snake->Score);

		int count = 0;
		FILE* pf = fopen("chart.txt", "r+");
		if (pf == NULL)
		{
			pf = fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中，chart.txt文件打开失败");
				exit(-1);
			}
			fprintf(pf, "%-5d\n%-10d\n", count + 1, snake->Score);
			fclose(pf);
		}
		else
		{
			rewind(pf);
			fscanf(pf, "%d", &count);
			//确定游玩次数
			count++;
			fseek(pf, 0, 2);
			fprintf(pf, "%-10d\n", snake->Score);
			rewind(pf);
			fprintf(pf, "%-5d", count);
			fclose(pf);
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status ==ESC)//如果是按Esc退出
	{
		system("cls");
		SetPos(45, 15);
		printf("已退出游戏");
		SetPos(0, 35);
	}
	//销毁蛇身链表
	DestSnake(snake);
	//初始化蛇
	SnakeInit(snake);//为下一次游玩做准备
}

void pause()
{
	while (1)//死循环执行暂停
	{
		Sleep(200);//暂停程序200毫秒
		if (KEY_PRESS(VK_SPACE))//直到再次按下空格，就结束暂停
			break;
	}
}

//游戏进行
void GameRun(Snake* snake)
{
	//打印游戏开始后界面
	PlayInte(snake);
	int i = 0;

	//用头插法 构建   初始   蛇身节点
	for (i = 5; i >= 1; i--)
	{
		SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));//申请新节点
		if (newnode == NULL)
		{
			printf("GameRun函数中malloc失败！");
			return;
		}
		newnode->x = SNAKE_HEAD_X - i * 2;//根据  初始蛇头坐标  控制链表尾节点的坐标
		newnode->y = SNAKE_HEAD_Y;        //因为是头插所以第一个插入的节点在插入完成后是最后一个节点
		if (snake->pSnake == NULL)
		{
			snake->pSnake = newnode;
			newnode->next = NULL;
		}
		else
		{
			newnode->next = snake->pSnake;
			snake->pSnake = newnode;
		}
	}

	//打印蛇身
	SnakePrint(snake);

	RandomFood(snake);
	//蛇走
	do
	{
		//打印会随游戏进行而变化的信息
		SetPos(65, 10);
		printf("当前分数：%-5d", snake->Score);
		SetPos(65, 11);
		printf("当前每一个食物的分数：%-2d", snake->foodWeight);
		SetPos(65, 13);
		printf("当前蛇的速度：%c(D为初始速度加速一次", (snake->SnakeSpeed - 200) / 40 + 'D');
		SetPos(65, 14);
		printf("为C，减速一次为E，最快为A)");
		if (KEY_PRESS(VK_UP)&&snake->Snakedir != DOWN)//按上并且 蛇在向下时不能直接向上走
		{
			snake->Snakedir = UP;//更改蛇的方向为上
		}
		else if (KEY_PRESS(VK_DOWN) && snake->Snakedir != UP)//下并且 蛇在向上时不能直接向下走
		{
			snake->Snakedir = DOWN;//更改蛇的方向为下
		}
		else if (KEY_PRESS(VK_LEFT) && snake->Snakedir != RIGHT)//左并且 蛇在向右时不能直接向左走
		{
			snake->Snakedir = LEFT;//更改蛇的方向为左
		}
		else if (KEY_PRESS(VK_RIGHT) && snake->Snakedir != LEFT)//右并且 蛇在向左时不能直接向右走
		{
			snake->Snakedir = RIGHT;//更改蛇的方向为右
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			pause();
		}
		else if(KEY_PRESS(VK_ESCAPE))//退出
		{
			//提示信息打印
			SetPos(20, 15);
			printf("若退出游戏当前游戏进度无法保存！！！");
			SetPos(20, 16);
			printf("确定退出游戏吗？(Y/N)【输入Y/N+回车】");
			char ch = 0;
			do
			{
				ch = getchar();
				if (ch == 'Y' || ch == 'y')
				{
					snake->status = ESC;//退出游戏  更改游戏状态为退出
					break;
				}
				if (ch == 'N' || ch == 'n')
				{
					//继续游戏
					SetPos(20, 15);
					//打印空格覆盖提示信息
					printf("                                    ");
					SetPos(20, 16);
					printf("                      ");
				}
			} while (ch!='Y' &&ch!='y' && ch != 'N' && ch != 'n');//控制输入符号有效性
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (snake->SnakeSpeed > 80)//不能一直加速
			{
				snake->SnakeSpeed -= 40;//加速蛇  暂停  时间减少
				snake->foodWeight += 2;//加速 吃掉一个食物的分数增加
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (snake->SnakeSpeed < 320)//不能一直减速
			{
				snake->SnakeSpeed += 40;//减速蛇  暂停  时间增加
				snake->foodWeight -= 2;//减速 吃掉一个食物的分数减少
			}
		}
		//休眠
		Sleep(snake->SnakeSpeed);//按完键后，蛇（程序）暂停一下，给玩家反应时间
		//走一步
		SnakeMove(snake);//根据按键情况走一步
	} while (snake->status == OK);//游戏状态要OK循环才继续
}

//比较函数
int compare(const void* p, const void* q)
{
	return *(int*)q - *(int*)p;
}

//打印排行榜
void ChartPrint(Snake* snake)
{
	//美化一下界面
	SetPos(0, 0);
	printf("---------------------------------------------");
	SetPos(45, 0);
	printf("__排行榜__");
	SetPos(55, 0);
	printf("---------------------------------------------");

	//打开游戏时存放分数的文件
	FILE* pf = fopen("chart.txt", "r");
	if (pf == NULL)
	{
		printf("ChartPrint函数中chart.txt文件打开失败！");
		exit(-1);
	}

	rewind(pf);//让文件读写指针来到  最开头，读取count的值

	int count = 0;
	fscanf(pf, "%d", &count);//读取count的值
	int i = 0;

	//打印提示信息
	SetPos(34, 1);
	printf("|名次  |                |总分      |\n");

	//申请空间存放分数
	int* tmp = (int*)malloc(sizeof(int) * count);
	for (i = 0; i < count; i++)//分数一共count个
	{
		int score = 0;
		fscanf(pf, "%d", &score);//读取分数
		tmp[i] = score;//将读取的分数放入tmp中
	}

	//定义供qsort比较的  比较函数
	int (*p)(const void*, const void*) = compare;

	//使用qsort函  降序  排序分数
	qsort(tmp, count, sizeof(int), p);
	for (i = 0; i < count; i++)
	{
		SetPos(34, i + 2);
		printf("|第%2d名|                |%-10d|\n", i + 1, tmp[i]);//打印分数
	}
	SetPos(34, i + 4);//将控制台光标放到合适的位置
	fclose(pf);//关闭文件
}

//初始化蛇
void SnakeInit(Snake* snake)
{
	snake->pSnake = NULL;
	snake->pFood = NULL;//游戏还未开始时没有食物
	snake->Score = 0;//初始分数为0
	snake->foodWeight = 10;//初始每个食物分数
	snake->SnakeSpeed = 200;//初始速度
	snake->status = OK;//游戏状态OK表示可以进行游戏
	snake->Snakedir = RIGHT;//蛇最开始的方向默认为右
}
void GameStart(Snake* snake)
{
	//设置游戏窗口大小
	system("mode con cols=100 lines=40");
	system("title 贪吃蛇 ");

	//获得控制台句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//创建保存光标信息的结构变量
	CONSOLE_CURSOR_INFO CursorInfo;
	//获取控制台光标信息
	GetConsoleCursorInfo(hOutput, &CursorInfo);
	//隐藏光标
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(hOutput, &CursorInfo);

	//初始化蛇
	SnakeInit(snake);
	//打印游戏开始前的界面
	Welcome();
	
	enum OPTION option = BEGIN;//默认选项为开始游戏
	do
    {
		    //打印选项界面
		    SetPos(40, 15);
			printf("1.开始游戏");
			SetPos(40, 16);
			printf("2.排行榜");
			SetPos(36, 18);
			printf("请按↑，↓选择选项，按Enter键确认选项");
			SetPos(36, 19);
			printf("按Esc退出游戏。");
			if (KEY_PRESS(VK_UP))//如果按了↑就进去
			{
				SetPos(38, 16);//按了↑，→就指向第15行
				printf("  ");//所以把第16行的→用空格覆盖
				SetPos(38, 15);
				printf("→");//再在第15行打印→

				option = BEGIN;//更改枚举变量的值为  开始
			}
			else if (KEY_PRESS(VK_DOWN))//如果按了↑就进去
			{
				SetPos(38, 15);//按了↓，→就指向第16行
				printf("  ");//所以把第15行的→用空格覆盖
				SetPos(38, 16);
				printf("→");//再在第16行打印→

				option = CHART;//更改枚举变量的值为  排行榜
			}

			if (option ==BEGIN)
			{
				SetPos(38, 16);
				printf("  ");
				SetPos(38, 15);
				printf("→");//解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)//如果选项状态为  开始 并且  按了Enter键就进去
				{
					system("cls");//清屏
					SetPos(35, 15);//设置控制台光标到  合适  的位置
					printf("开始游戏");//打印提示信息
					SetPos(35, 17);
					system("pause");//达成  按任意键继续。。。 的效果
					system("cls");
					GameRun(snake);//游戏进行
					GameEnd(snake);//游戏善后
					system("cls");
				}
			}
			if (option == CHART)
			{
				SetPos(38, 15);
				printf("  ");
				SetPos(38, 16);
				printf("→");//解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)//如果选项状态为  排行榜 并且  按了Enter键就进去
				{
					system("cls");//清屏
					SetPos(35, 15);
					printf("排行榜");
					SetPos(35, 17);
					system("pause");//达成  按任意键继续。。。 的效果
					system("cls");
					//排行榜
					ChartPrint(snake);//打印排行榜
					system("pause");
					system("cls");
				}
			}
		} while (KEY_PRESS(VK_ESCAPE) != 1);//如果按Esc就结束循环
}


